/**
 * Log4jLog
 *
 * @author Chris Pratt
 *
 * 10/23/2007
 */
package com.anodyzed.onyx.log.jul;

import com.anodyzed.onyx.bean.BeanUtils;
import com.anodyzed.onyx.cache.Cache;
import com.anodyzed.onyx.cache.CacheLoader;
import com.anodyzed.onyx.cache.MRUCache;
import com.anodyzed.onyx.cache.SimpleCache;
import com.anodyzed.onyx.log.Log;
import com.anodyzed.onyx.log.LogLevel;
import com.anodyzed.onyx.text.TextFormat;
import com.anodyzed.onyx.util.SplayTreeMap;

import java.beans.IntrospectionException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Java 5 java.util.logging Facade - Allows using format strings and variable 
 * argument lists to build log messages efficiently. 
 *
 * Since there is no up-front string processing used to construct the log
 * message, if the logger is set above the level of the log method called, it is
 * quickly and quietly ignored, with no processing.
 *
 * If, on the other hand, the log level is below the level of the log method
 * called, the format string is processed and the supplied arguments are used to
 * build the log message.
 *
 * The Log message format string is comprised of static text with interspersed
 * replacement argument placeholders.  The placeholders begin with '{' and end
 * with '}'.
 *
 * Example:
 *   <code>"Simple {0}, More Complex {1.lastname}, Even More Complex
 *   {2.address.city}, Most Complex {1.birthDate,date,yyyyMMdd}"</code>
 *
 * The simplest is just the (zero based) index of the argument used to replace
 * the placeholder.  Additionally you can add a dot separated string of bean
 * property names to access from the indexed argument. Lastly, you can use the
 * java.text.MessageFormat style comma separated format descriptors to format
 * each replacement element.
 *
 * Example:
 *   <code>log.debug("Simple {0}, More Complex {1.lastname}, Even More Complex
 *   {2.address.city}, Most Complex {1.birthDate,date,yyyyMMdd}",
 *   5, user, company);</code>
 *
 * Might write out:
 *   <code>Simple 5, More Complex Chris, Even More Complex Sacramento,
 *   Most Complex 19640123</code>
 *
 * Oh, one last thing, if the last argument is an instance of Throwable, it will
 * be passed on to java.util.logging's Throwable support 
 *  
 * @see com.anodyzed.onyx.text.TextFormat 
 */
public class JULLog implements Log {
  private static final String FQCN = JULLog.class.getName();
  private static final TextFormat BEAN_PROPERTY = new TextFormat("  {0}: {1}");

  private static final Cache<String,TextFormat> formatCache = new MRUCache<>(new SimpleCache<>("formatCache",null,new CacheLoader<String,TextFormat>() {
    @Override
    public TextFormat load (Object arg,String key) {
      return new TextFormat(key);
    } //load
  },new SplayTreeMap<String,TextFormat>()),100);

  private Logger log;

  /**
   * Constructor
   *
   * @param log The Log4j Logger
   */
  public JULLog (Logger log) {
    this.log = log;
  } //Log4jLog

  /**
   * Get the wrapped Log4j Logger
   *
   * @return Log4j Logger
   */
  public Logger getWrappedLogger () {
    return log;
  } //getWrappedLogger

  /**
   * Convert the LogLevel to the Log4j Level
   *
   * @param lvl The ELF4j LogLevel
   * @return The Log4j Log Level
   */
  private Level level (LogLevel lvl) {
    switch(lvl) {
      case Debug:
        return Level.CONFIG;
      case Trace:
        return Level.FINE;
      case Info:
        return Level.INFO;
      case Warn:
        return Level.WARNING;
      case Error:
        return Level.SEVERE;
      case None:
        return Level.OFF;
      case All:
        return Level.ALL;
    }
    return null;
  } //level

  /** 
   * Find the Stack Trace Element of the caller of the log statement 
   *
   * @return The current Class & Method Names
   */
  private String[] getCaller () {
    int i = 0;
    StackTraceElement[] elements = Thread.currentThread().getStackTrace();
    while(i < elements.length) {
      if(elements[i++].getClassName().equals(FQCN)) {
        while(i < elements.length) {
          if(!elements[i].getClassName().equals(FQCN)) {
            String[] caller = new String[2];
            caller[0] = elements[i].getClassName();
            caller[1] = elements[i].getMethodName();
            return caller;
          }
          ++i;
        }
      }
    }
    return null;
  } //getCaller

  /**
   * Write the formatted message (and possible Throwable) to the log using the 
   * pre-parsed <code>TextFormat</code> 
   *
   * @param lvl The Message Priority Level
   * @param fmt The <code>TextFormat</code> of the message
   * @param args Replacement Arguments 
   */
  private void write (Level lvl,TextFormat fmt,Object... args) {
    String[] caller = getCaller();
    try {
      String msg = fmt.format(args);
      Throwable t = ((args != null) && (args.length > 0) && (args[args.length - 1] instanceof Throwable)) ? (Throwable)args[args.length - 1] : null;
      if(t != null) {
        log.logp(lvl,caller[0],caller[1],msg,t);
      } else {
        log.logp(lvl,caller[0],caller[1],msg);
      }
    } catch(IndexOutOfBoundsException x) {
      log.logp(Level.SEVERE,caller[0],caller[1],"Invalid Index in Log Statement \"" + fmt + '\"',x);
    } catch(IllegalArgumentException x) {
      log.logp(Level.SEVERE,caller[0],caller[1],"Invalid Argument in Log Statement \"" + fmt + '\"',x);
    }
  } //write

  /**
   * Log all the Readable Properties in a Bean
   *
   * @param lvl The Logging Level
   * @param msg The lead-in message [optional]
   * @param bean The Bean to Log 
   */
  private void writeBean (Level lvl,String msg,Object bean) {
    if(isEnabledFor(lvl)) {
      String[] caller = getCaller();
      if(msg != null) {
        log.logp(lvl,caller[0],caller[1],formatCache.get(msg).format(bean));
      }
      if(bean != null) {
        try {
          for(Method method : BeanUtils.getAccessors(bean.getClass())) {
            try {
              log.logp(lvl,caller[0],caller[1],BEAN_PROPERTY.format((Object)method.getName(),method.invoke(bean)));
            } catch(IllegalAccessException | InvocationTargetException x) {
              // fallthru
            }
          }
        } catch(IntrospectionException x) {
          // fallthru
        }
      } else {
        log.logp(lvl,caller[0],caller[1],"<null>");
      }
    }
  } //writeBean

  /**
   * Test whether the logger is enabled for the suppled Logging Level
   *
   * @param lvl The java.util.logging.Level in question
   * @return true if enabled
   */
  private boolean isEnabledFor (Level lvl) {
    return log.isLoggable(lvl);
  } //isEnabledFor

  /**
   * Test whether the logger is enabled for the supplied Logging Priority Level
   *
   * @param lvl The Logging Priority Level in question
   * @return true if enabled
   */
  @Override
  public boolean isEnabledFor (LogLevel lvl) {
    return log.isLoggable(level(lvl));
  } //isEnabledFor

  /**
   * Write a Log Message at the supplied Log4j Priority
   *
   * @param lvl The Logging Priority Level
   * @param fmt The Log message format string
   * @param args The replacement arguments
   */
  @Override
  public void log (LogLevel lvl,String fmt,Object... args) {
    if(isEnabledFor(lvl)) {
      write(level(lvl),formatCache.get(fmt),args);
    }
  } //log

  /**
   * Write a Log Message at the supplied Log4j Priority
   *
   * @param lvl The Logging Priority Level
   * @param fmt The Log message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void log (LogLevel lvl,TextFormat fmt,Object... args) {
    if(isEnabledFor(lvl)) {
      write(level(lvl),fmt,args);
    }
  } //log

  /**
   * Log all the Readable Properties in a Bean
   *
   * @param lvl The Logging Priority Level
   * @param msg The lead in Message 
   * @param bean The Bean to Log 
   */
  @Override
  public void bean (LogLevel lvl,String msg,Object bean) {
    writeBean(level(lvl),msg,bean);
  } //bean

  /**
   * Check whether Trace Logging is Enabled
   *
   * @return true if trace logging is enabled
   */
  @Override
  public boolean isTraceEnabled () {
    return log.isLoggable(Level.FINE);
  } //isTraceEnabled

  /**
   * Write out a trace level message
   *
   * @param fmt The Log Message Format String
   * @param args The replacement arguments
   */
  @Override
  public void trace (String fmt,Object... args) {
    if(log.isLoggable(Level.FINE)) {
      write(Level.FINE,formatCache.get(fmt),args);
    }
  } //trace

  /**
   * Write out a trace level message
   *
   * @param fmt The Log Message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void trace (TextFormat fmt,Object... args) {
    if(log.isLoggable(Level.FINE)) {
      write(Level.FINE,fmt,args);
    }
  } //trace

  /**
   * Write out the bean properties at the trace level
   *
   * @param msg A descriptive message
   * @param bean The Bean being debugged
   */
  @Override
  public void traceBean (String msg,Object bean) {
    if(log.isLoggable(Level.FINE)) {
      writeBean(Level.FINE,msg,bean);
    }
  } //traceBean

  /**
   * Check whether Debug Logging is enabled
   *
   * @return true if debug logging is enabled
   */
  @Override
  public boolean isDebugEnabled () {
    return log.isLoggable(Level.CONFIG);
  } //isDebugEnabled

  /**
   * Write out a debug level message
   *
   * @param fmt The Log message format string
   * @param args The replacement arguments
   */
  @Override
  public void debug (String fmt,Object... args) {
    if(log.isLoggable(Level.CONFIG)) {
      write(Level.CONFIG,formatCache.get(fmt),args);
    }
  } //debug

  /**
   * Write out a debug level message
   *
   * @param fmt The Log Message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void debug (TextFormat fmt,Object... args) {
    if(log.isLoggable(Level.CONFIG)) {
      write(Level.CONFIG,fmt,args);
    }
  } //debug

  /**
   * Write out the bean properties at the debug level
   *
   * @param msg A descriptive message
   * @param bean The Bean being debugged
   */
  @Override
  public void debugBean (String msg,Object bean) {
    if(log.isLoggable(Level.CONFIG)) {
      writeBean(Level.CONFIG,msg,bean);
    }
  } //debugBean

  /**
   * Check whether informational logging is enabled
   *
   * @return true if enabled
   */
  @Override
  public boolean isInfoEnabled () {
    return log.isLoggable(Level.INFO);
  } //isInfoEnabled

  /**
   * Write out an informational level message
   *
   * @param fmt The Log message format string
   * @param args The replacement arguments
   */
  @Override
  public void info (String fmt,Object... args) {
    if(log.isLoggable(Level.INFO)) {
      write(Level.INFO,formatCache.get(fmt),args);
    }
  } //info

  /**
   * Write out an informational level message
   *
   * @param fmt The Log Message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void info (TextFormat fmt,Object... args) {
    if(log.isLoggable(Level.INFO)) {
      write(Level.INFO,fmt,args);
    }
  } //info

  /**
   * Check whether warning logging is enabled
   *
   * @return true if enabled
   */
  @Override
  public boolean isWarnEnabled () {
    return log.isLoggable(Level.WARNING);
  } //isWarnEnabled

  /**
   * Write out a warning level message
   *
   * @param fmt The Log message format string
   * @param args The replacement arguments
   */
  @Override
  public void warn (String fmt,Object... args) {
    if(log.isLoggable(Level.WARNING)) {
      write(Level.WARNING,formatCache.get(fmt),args);
    }
  } //warn

  /**
   * Write out a warning level message
   *
   * @param fmt The Log Message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void warn (TextFormat fmt,Object... args) {
    if(log.isLoggable(Level.WARNING)) {
      write(Level.WARNING,fmt,args);
    }
  } //warn

  /**
   * Write out an error level message
   *
   * @param fmt The log message format string
   * @param args The replacement arguments
   */
  @Override
  public void error (String fmt,Object... args) {
    if(log.isLoggable(Level.SEVERE)) {
      write(Level.SEVERE,formatCache.get(fmt),args);
    }
  } //error

  /**
   * Write out an error level message
   *
   * @param fmt The Log Message <code>TextFormat</code>
   * @param args The replacement arguments
   */
  @Override
  public void error (TextFormat fmt,Object... args) {
    if(log.isLoggable(Level.SEVERE)) {
      write(Level.SEVERE,fmt,args);
    }
  } //error

  /**
   * Get the Effective Level for this Logger
   *
   * @return Logging Level
   */
  @Override
  public LogLevel getEffectiveLevel () {
    switch(log.getLevel().intValue()) {
      case 700:               // Level.CONFIG:
        return LogLevel.Debug;
      case 500:               // Level.FINE:
        return LogLevel.Trace;
      case 800:               // Level.INFO:
        return LogLevel.Info;
      case 900:               // Level.WARNING:
        return LogLevel.Warn;
      case 1000:              // Level.SEVERE:
        return LogLevel.Error;
      case Integer.MIN_VALUE: // Level.ALL:
        return LogLevel.All;
      case Integer.MAX_VALUE: // Level.OFF:
        return LogLevel.None;
    }
    return null;
  } //getEffectiveLevel

} //*Log4jLogger
